package clang;

import java.util.ArrayList;
import java.util.Locale;
import java.util.Scanner;
import java.util.regex.MatchResult;
import java.util.regex.Pattern;

public class CLangParser {
    
    public static final String SHORT_HELP = 
            "<html>" +
            "<p>A FIR filter expression is a sum of products.</p>" + 
            "<p>A product consists of a floating-point constant multiplied by an array element:</p><blockquote>FLOATINGPOINT_LITERAL * BUFFER_NAME[INDEX]</blockquote>" + 
            "<p>A IIR filter expression begins with \"BUFFER_NAME[0] =\", and is followed by a sum of products.</p>" +
            "<p>You can generate random filters and see their expressions:</p>" +
            "<ol>" + 
            "<li>Close window <b>Digital filter expression</b></li>" +
            "<li>Generate random filter (FIR or IIR)</li>" +
            "<li>Open window <b>Digital filter expression</b></li>" +
            "</ol></html>";

    /**
     * This type of buffer does not carry interval information.
     */
    public static class Buffer {

        private String name;
        private float[] coefs;

        public Buffer(int length) {
            coefs = new float[length];
        }

        public String getName() {
            return name;
        }

        public int length() {
            return coefs.length;
        }

        public float getCoef(int index) {
            return coefs[index];
        }
    }
    private String expression;
    private Scanner scanner;
    private ArrayList<Product> productList;
    private Buffer inputBuffer;
    private Buffer outputBuffer;

    private static class Product {

        float coefficient;
        String bufferName;
        int index;

        public String getBufferName() {
            return bufferName;
        }

        public void setBufferName(String bufferName) {
            this.bufferName = bufferName;
        }

        public float getCoefficient() {
            return coefficient;
        }

        public void setCoefficient(float coefficient) {
            this.coefficient = coefficient;
        }

        public int getIndex() {
            return index;
        }

        public void setIndex(int index) {
            this.index = index;
        }
    }

    public String getExpression() {
        return expression;
    }

    public Buffer getInputBuffer() {
        return inputBuffer;
    }

    public Buffer getOutputBuffer() {
        return outputBuffer;
    }

    public CLangParser(String expression) {
        this.expression = expression.replace(" ", ""); // remove any whitespace
    }

    public void run() throws ParseException {
        inputBuffer = null;
        outputBuffer = null;

        boolean recursive;
        {
            int i = expression.indexOf('=');
            recursive = (i >= 0);
        }

        scanner = new Scanner(expression);
        scanner.useLocale(Locale.US);

        productList = new ArrayList<Product>();

        if (!recursive) {
            // FIR
            while (true) {
                try {
                    Product product = nextProduct();
                    if (product != null) {
                        productList.add(product);
                    } else {
                        break;
                    }
                } catch (ParseException ex) {
                    throw ex;
                } catch (Exception ex) {
                    ex.printStackTrace();
                    break;
                }
            }
            ArrayList<String> nameList = getBufferNameList();
            int n = nameList.size();
            if (n == 1) {
                String name = productList.get(0).getBufferName();
                inputBuffer = buildBuffer(name, false);
            } else {
                throw new ParseException(n + " buffer name(s) identified (must be 1)");
            }
        } else {
            // IIR
            String outputBufferName = getOutputBufferName();
            while (true) {
                try {
                    Product product = nextProduct();
                    if (product != null) {
                        productList.add(product);
                    } else {
                        break;
                    }
                } catch (ParseException ex) {
                    throw ex;
                } catch (Exception ex) {
                    ex.printStackTrace();
                    break;
                }
            }
            ArrayList<String> nameList = getBufferNameList();
            int n = nameList.size();
            if (n == 2) {
                int outputBufferNameIndex = Integer.MIN_VALUE;
                for (int i = 0; i < n; i++) {
                    if (nameList.get(i).equals(outputBufferName)) {
                        outputBufferNameIndex = i;
                        break;
                    }
                }
                if (outputBufferNameIndex >= 0) {
                    int inputBufferNameIndex = n - outputBufferNameIndex - 1;
                    String inputBufferName = nameList.get(inputBufferNameIndex);
                    inputBuffer = buildBuffer(inputBufferName, false);
                    outputBuffer = buildBuffer(outputBufferName, true);
                } else {
                    throw new ParseException("Output buffer name misidentification");
                }
            } else {
                throw new ParseException(n + " buffer name(s) identified (must be 2)");
            }
        }
    }

    /**
     * TODO Use global Pattern instances?
     */
    private Product nextProduct() throws ParseException {
        Product product = null;

        MatchResult matchResult;
        int start, end;

        /**
         * Scanner hasNext.../skip work with tokens.
         * A token is separated by delimiters.
         * Pay attention to delimiters (not only patterns).
         */
        // get additive operator
        scanner.useDelimiter("[^\\+\\-]");
        boolean negative;
        if (productList.isEmpty()) {
            // +/- is not required
            String pattern = "[\\+\\-]?";
            if (scanner.hasNext(pattern)) {
                scanner.skip(pattern);
                matchResult = scanner.match();
                start = matchResult.start();
                end = matchResult.end();
                if (start == end) {
                    negative = false;
                } else {
                    String s = expression.substring(start, end);
                    negative = (s.compareTo("-") == 0);
                }
            } else {
                // Do not throw ParseException
                return null;
            }
        } else {
            // +/- is required
            String pattern = "[\\+\\-]";
            if (scanner.hasNext(pattern)) {
                scanner.skip(pattern);
                matchResult = scanner.match();
                start = matchResult.start();
                end = matchResult.end();
                String s = expression.substring(start, end);
                negative = (s.compareTo("-") == 0);
            } else {
                // Do not throw ParseException
                return null;
            }
        }

        // get floating-point literal
        //scanner.skip("(\\s)*"); // skip white space
        scanner.useDelimiter("[\\s\\*f]"); // stop at '*'
        float coefficient;
        if (scanner.hasNextFloat()) {
            // get floating point value
            coefficient = scanner.nextFloat();
            if (coefficient <= 0.0f) {
                throw new ParseException(
                        "Cannot parse positive floating-point value");
            }
        } else {
            throw new ParseException("scanner.hasNextFloat() == false");
        }

        // jump over the floating-point sufix f
        scanner.skip("f?");

        // jump over the operator * and the surrounding white characters
        scanner.skip("(\\s)*(\\*)*(\\s)*"); // \G - The end of the previous match

        // get buffer name
        String bufferName;
        {
            String regex = "(\\w)+";
            Pattern pattern = Pattern.compile(regex);
            scanner.useDelimiter("[\\W]");
            if (scanner.hasNext(pattern)) {
                scanner.skip(pattern);
                matchResult = scanner.match();
                start = matchResult.start();
                end = matchResult.end();
                bufferName = expression.substring(start, end);
            } else {
                throw new ParseException("No such pattern: " + regex);
            }
        }

        // skip "["
        scanner.useDelimiter("[^\\[]");
        skipPattern("\\[");

        scanner.useDelimiter("[\\s\\]]");
        int index;
        if (scanner.hasNextInt()) {
            index = scanner.nextInt();
        } else {
            throw new ParseException("Cannot read index");
        }

        // skip "]"
        scanner.useDelimiter("[^\\]]");
        skipPattern("\\]");

        // build product
        product = new Product();
        if (negative) {
            coefficient = -coefficient;
        }
        product.setCoefficient(coefficient);
        product.setBufferName(bufferName);
        product.setIndex(index);

        return product;
    }

    private String getOutputBufferName() throws ParseException {
        Pattern pattern;

        // get buffer name
        String bufferName;
        scanner.useDelimiter("[\\s\\[\\]]"); // \\W ?
        String regex = "(\\w)+";
        pattern = Pattern.compile(regex);
        if (scanner.hasNext(pattern)) {
            scanner.skip(pattern);

            MatchResult matchResult = scanner.match();
            int start = matchResult.start();
            int end = matchResult.end();
            bufferName = expression.substring(start, end);
        } else {
            throw new ParseException("No such pattern: " + regex);
        }

        // skip "[0]"
        scanner.useDelimiter("[\\s\\=]");
        skipPattern("\\Q[0]\\E");

        // skip "="
        scanner.useDelimiter("[^\\=]");
        skipPattern("\\=");

        return bufferName;
    }

    /**
     * NOTE Set scanner delimiter <i>a priori</i>
     * @param regex
     * @throws ParseException
     */
    private void skipPattern(String regex) throws ParseException {
        Pattern pattern = Pattern.compile(regex);
        if (scanner.hasNext(pattern)) {
            scanner.skip(pattern);
        } else {
            throw new ParseException("No such pattern: " + regex);
        }
    }

    /**
     *
     * @param name buffer name
     */
    private Buffer buildBuffer(String name, boolean recursive) throws ParseException {
        int count = 0;

        // count
        // search greatest index
        int maxIndex = Integer.MIN_VALUE;
        for (Product p : productList) {
            if (p.getBufferName().compareTo(name) == 0) {
                count++;
                maxIndex = Math.max(maxIndex, p.getIndex());
            }
        }

        // set length
        // max index must >= 0
        if (maxIndex < 0) {
            throw new ParseException("Negative index");
        }
        if (recursive) {
            // index 0 is reserved
            // max index must be length
            if (maxIndex != count) {
                throw new ParseException("Index too large (in output buffer)");
            }
        } else {
            // max index must be length
            if (maxIndex != (count - 1)) {
                throw new ParseException("Index too large (in input buffer)");
            }
        }
        int length = maxIndex + 1;

        Float[] _coefs = new Float[length];
        for (int i = 0; i < _coefs.length; i++) {
            _coefs[i] = null;
        }

        // set coef
        final int min = recursive ? 1 : 0;
        final int max = _coefs.length - 1;
        for (Product p : productList) {
            if (p.getBufferName().compareTo(name) == 0) {
                int index = p.getIndex();
                if ((index >= min) && (index <= max)) {
                    if (_coefs[index] == null) {
                        _coefs[index] = p.getCoefficient();
                    } else {
                        throw new ParseException("Index conflict (" + index + ")");
                    }
                } else {
                    throw new ParseException("Invalid index (" + index + ")");
                }
            }
        }

        if (recursive) {
            _coefs[0] = 1.0f;
        }

        // checking
        for (int i = 0; i < _coefs.length; i++) {
            if (_coefs[i] == null) {
                throw new ParseException("Undefined coefficient");
            }
        }

        Buffer buffer = new Buffer(length);

        buffer.name = name;

        for (int i = 0; i < _coefs.length; i++) {
            // unboxing of null Float causes null ptr exception
            buffer.coefs[i] = _coefs[i];
        }

        return buffer;
    }

    private ArrayList<String> getBufferNameList() {
        ArrayList<String> nameList = new ArrayList<String>();

        for (Product p : productList) {
            if (!nameList.contains(p.getBufferName())) {
                nameList.add(p.getBufferName());
            }
        }

        return nameList;
    }
}
